//-----------------------------------------------------------------------
// <copyright file="MockObject.cs" company="NMock2">
//
//   http://www.sourceforge.net/projects/NMock2
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.
// </copyright>
//-----------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using NMock2.Matchers;
using NMock2.Monitoring;

namespace NMock2.Internal
{
	/// <summary>
	/// 
	/// </summary>
	public class MockObject : IInvokable, IMockObject
	{
		/// <summary>
		/// Stores the backlink to the mockery which created this mock object.
		/// </summary>
		private readonly Mockery mockery;

		/// <summary>
		/// Stores the mocked type(s).
		/// </summary>
		private readonly CompositeType mockedTypes;

		/// <summary>
		/// Stores the name of the mock object.
		/// </summary>
		private readonly string name;

		/// <summary>
		/// Stores the event handlers that could be added to the mock object.
		/// </summary>
		private readonly Dictionary<string, List<Delegate>> eventHandlers;

		/// <summary>
		/// Results that have been explicitly assigned via a call to a property setter.
		/// These will be returned for all subsequent calls to the matching property getter.
		/// </summary>
		private readonly Dictionary<string, object> assignedPropertyResults = new Dictionary<string, object>();

		/// <summary>
		/// Results that have been generated for methods or property getters.
		/// These will be returned for all subsequent calls to the same member.
		/// </summary>
		private readonly Dictionary<MethodInfo, object> rememberedMethodResults = new Dictionary<MethodInfo, object>();

		/// <summary>
		/// Initializes a new instance of the <see cref="MockObject"/> class.
		/// This constructor is needed by the <see cref="InterfaceOnlyMockObjectFactory"/> (the IL generation has to be changed!)
		/// </summary>
		/// <param name="mockery">The mockery.</param>
		/// <param name="mockedType">Type of the mocked.</param>
		/// <param name="name">The name.</param>
		protected MockObject(Mockery mockery, Type mockedType, string name)
			: this(mockery, new CompositeType(new Type[] { mockedType }), name, MockStyle.Default)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="MockObject"/> class.
		/// </summary>
		/// <param name="mockery">The mockery.</param>
		/// <param name="mockedType">Type of the mocked.</param>
		/// <param name="name">The name.</param>
		/// <param name="mockStyle">The mock style.</param>
		protected MockObject(Mockery mockery, CompositeType mockedType, string name, MockStyle mockStyle)
		{
			this.mockery = mockery;
			MockStyle = mockStyle;
			this.name = name;
			eventHandlers = new Dictionary<string, List<Delegate>>();
			mockedTypes = mockedType;
		}

		public string MockName
		{
			get { return name; }
		}

		protected CompositeType MockedTypes
		{
			get { return mockedTypes; }
		}

		protected Mockery Mockery
		{
			get { return mockery; }
		}

		/// <summary>
		/// Gets the mock style of this mock.
		/// </summary>
		protected MockStyle MockStyle { get; private set; }

		public override string ToString()
		{
			return name;
		}

		private Dictionary<string, object> rememberedResults = new Dictionary<string, object>();

		public void Invoke(Invocation invocation)
		{
			switch (MockStyle)
			{
				case MockStyle.Default:
				case MockStyle.Transparent:
					mockery.Dispatch(invocation);
					break;

				case MockStyle.Stub:
					{
						if (mockery.HasExpectationForIgnoringIsActive(invocation))
						{
							goto case MockStyle.Default;
						}

						// remember values set in a property setter
						if (invocation.Method.Name.StartsWith("set_"))
						{
							string getter = "get_" + invocation.Method.Name.Substring(4);
							if (assignedPropertyResults.ContainsKey(getter))
							{
								assignedPropertyResults[getter] = invocation.Parameters[0];
							}
							else
							{
								assignedPropertyResults.Add(getter, invocation.Parameters[0]);
							}

							return;
						}

						if (invocation.Method.Name.StartsWith("add_"))
						{
							AddEventHandler(invocation.Method.Name.Substring(4), (Delegate) invocation.Parameters[0]);
							return;
						}

						if (invocation.Method.Name.StartsWith("remove_"))
						{
							RemoveEventHandler(invocation.Method.Name.Substring(7), (Delegate) invocation.Parameters[0]);
							return;
						}

						// check whether we already have a value for this call
						object result;

						if (assignedPropertyResults.ContainsKey(invocation.Method.Name))
						{
							result = assignedPropertyResults[invocation.Method.Name];
						}
						else if (rememberedMethodResults.ContainsKey(invocation.Method))
						{
							result = rememberedMethodResults[invocation.Method];
						}
						else
						{
							result = GetStubResult(invocation);
							rememberedMethodResults.Add(invocation.Method, result);
						}

						if (result != Missing.Value)
						{
							invocation.Result = result;
						}

						break;
					}
				case MockStyle.RecursiveStub:
                    {
                        if (mockery.HasExpectationFor(invocation))
                        {
                            goto case MockStyle.Default;
                        }

                        object result;
                        if (rememberedResults.ContainsKey(invocation.Method.Name))
                        {
                            result = rememberedResults[invocation.Method.Name];
                        }
                        else
                        {
                            result = GetStubResult(invocation);
                            rememberedResults.Add(invocation.Method.Name, result);
                        }

                        if (result != Missing.Value)
                        {
                            invocation.Result = result;
                        }

                        break;
                    }
            }
        }

		/// <summary>
		/// Gets the default result for an invocation.
		/// </summary>
		/// <param name="invocation">The invocation.</param>
		/// <returns>The default value to return as result of the invocation. 
		/// <see cref="Missing.Value"/> if no default value was provided.</returns>
		private object GetStubResult(Invocation invocation)
		{
			Type returnType = invocation.Method.ReturnType;

			// void method
			if (returnType == typeof(void))
			{
				return Missing.Value;
			}

			// see if developer provides a return value
			object returnValue = mockery.ResolveType(invocation.Receiver, returnType);

			if (returnValue != Missing.Value)
			{
				return returnValue;
			}

			if (returnType.IsValueType)
			{
				// use default contructor for value types
				return Activator.CreateInstance(returnType);
			}

			if (returnType == typeof(string))
			{
				// string empty for strings
				return string.Empty;
			}

			if (returnType.IsClass && returnType.GetInterface("IEnumerable", false) != null)
			{
				// for enumerables (List, Dictionary) we create an empty object
				return Activator.CreateInstance(returnType);
			}

			if (returnType.IsSealed)
			{
				// null for sealed classes
				return null;
			}

			// a mock for interfaces and all cases no covered above
			return mockery.NewNamedMock(returnType, GetMemberName(invocation), mockery.GetDependencyMockStyle(invocation.Receiver, returnType) ?? MockStyle);
		}

		/// <summary>
		/// Gets the name of the member to be used as the name for a mock returned an a call to a stub.
		/// </summary>
		/// <param name="invocation">The invocation.</param>
		/// <returns>Name of the mock created as a result value on a call to a stub.</returns>
		private string GetMemberName(Invocation invocation)
		{
			StringWriter writer = new StringWriter();
			invocation.DescribeTo(writer);
			return writer.ToString();
		}


		public bool HasMethodMatching(Matcher methodMatcher)
		{
			return mockedTypes.GetMatchingMethods(methodMatcher, true).Count > 0;
		}

		public IList<MethodInfo> GetMethodsMatching(Matcher methodMatcher)
		{
			return mockedTypes.GetMatchingMethods(methodMatcher, false);
		}

		public void AddExpectation(IExpectation expectation)
		{
			mockery.AddExpectation(expectation);
		}

		public void AddEventHandler(string eventName, Delegate handler)
		{
			if (eventHandlers.ContainsKey(eventName))
			{
				List<Delegate> handlers = eventHandlers[eventName];

				//if (handlers == null)
				//{
				//    handlers = new List<Delegate>();
				//    eventHandlers.Add(eventName, handlers);
				//}

				if (!handlers.Contains(handler))
				{
					handlers.Add(handler);
				}
			}
			else
			{
				eventHandlers.Add(eventName, new List<Delegate> { handler });
			}
		}

		public void RaiseEvent(string eventName, params object[] args)
		{

			if (eventHandlers.ContainsKey(eventName))
			{
				IEnumerable handlers = eventHandlers[eventName];
				// copy handlers before invocation to prevent colection modified exception if event handler is removed within event handler itself.
				List<Delegate> delegates = new List<Delegate>();
				foreach (Delegate handler in handlers)
				{
					delegates.Add(handler);
				}

				foreach (Delegate d in delegates)
				{
					try
					{
						d.DynamicInvoke(args);
					}
					catch (TargetInvocationException tie)
					{
						// replace stack trace with original stack trace
						FieldInfo remoteStackTraceString = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);
						remoteStackTraceString.SetValue(tie.InnerException, tie.InnerException.StackTrace + Environment.NewLine);
						throw tie.InnerException;
					}
				}
			}
			else
			{
				// check whether the mocked type has this event
				if (!HasMethodMatching(new MethodNameMatcher("add_" + eventName)))
				{
					throw new ArgumentException(string.Format("Event with name {0} does not exist on type(s) {1}.", eventName, mockedTypes), "eventName");
				}
			}
		}

		public void RemoveEventHandler(string eventName, Delegate handler)
		{
			if (eventHandlers.ContainsKey(eventName))
			{
				List<Delegate> handlers = eventHandlers[eventName];
				handlers.Remove(handler);
				if (handlers.Count == 0)
				{
					eventHandlers.Remove(eventName);
				}
			}
		}
	}
}
